#!/usr/bin/python
#
#  ulcopyiso.py - copy ISO file to usbextreme backup disk
#
#  I wrote this tool because I don't have Windows and still want to copy
#  backups or demos over to my USB disk using Linux.  Unfortunately the
#  usbextreme tools (ul_format.exe, ul_install.exe) are rather cryptic and
#  not well documented.
#
#
#  usbextreme format
#  =================
#
#  The hard disk partition is FAT formatted.  The usbextreme guys tell you
#  to use Windows NTFS formatting (followed by ul_format.exe), which is
#  probably to get a large partition and to get the FAT particulars right.
#  I'm not sure whether basic Linux VFAT format is OK, so I used the
#  recommended method (Windows NTFS + ul_format.exe).
#
#  Index of backups is in file named 'ul.cfg', while individual backup ISO
#  images are split into parts of maximum 1GB size.  ISO file parts are named
#  as follows:
#
#    ul.<crc>.<elfname>.<nn>
#
#  where
#
#    <crc>       32-bit CRC of the *display name* (name shown in
#                usbextreme UI) encoded as uppercase hex digits (see crc())
#
#    <elfname>   Name of ELF startup file on ISO, read from ISO image
#                'system.cnf' file
#
#    <nn>        Part index, '00', '01', ...
#
#  For instance, a particular backup might consist of five files:
#
#    ul.4934399A.SLES_123.45.00       (1073741824 bytes)
#    ul.4934399A.SLES_123.45.01       (1073741824 bytes)
#    ul.4934399A.SLES_123.45.02       (1073741824 bytes)
#    ul.4934399A.SLES_123.45.03       (1073741824 bytes)
#    ul.4934399A.SLES_123.45.04       (4063232 bytes)
#
#  The crc is computed over the display name, which is somewhat bizarre.
#  See crc() below for details of the computation.
#
#  Format of 'ul.cfg':  The file consists of 64-byte structures, one for
#  each backup, in the order shown in the usbextreme UI.  The contents of
#  each 64-byte record is as follows:
#
#    32 bytes            Display name, truncated to 32 bytes, padded with
#                        zeroes
#
#    15 bytes            Some sort of base name, format is
#
#                            ul.<elfname>
#
#                        For instance, in the case of our imaginary backup,
#                        this name is 'ul.SLES_123.45'.  Note that there
#                        *is* a 'ul.' prefix, but the CRC is not present.
#
#    1 byte              Number of parts, 0x05 in this example
#
#    16 bytes            Unknown, the following fixed 16 (hex) bytes seem
#                        to work fine:
#
#                            14 00 00 00  00 08 00 00
#                            00 00 00 00  00 00 00 00
#
#  That's about it.  Note that this tool simply copies over stuff to the
#  drive, but cannot delete anything.  Adding that is pretty trivial, just
#  remove the data files and the corresponding entry from the 'ul.cfg'
#  index.

import sys, os, random, struct, tempfile

def crcTable():
    lookuptab = [0L for i in range(256)]
    for i in xrange(256):
        t = (long(i) << 24L) & 0xffffffffL
        for j in xrange(8):
            if (t & 0x80000000L == 0):
                t = (t << 1L) & 0xffffffffL
            else:
                t = (((t << 1L) & 0xffffffffL) ^ 0x04c11db7L) & 0xffffffffL
        lookuptab[i] = t
    return lookuptab

def crc(str):
    tab = crcTable()
    res = 0L
    for i in xrange(len(str)):
        t = long(ord(str[i])) & 0xffL
        t = (t ^ (res >> 24L)) & 0xffffffffL
        res = (res << 8L) & 0xffffffffL
        res = (res ^ tab[t]) & 0xffffffffL
    return struct.pack('!I', res).encode('hex').upper()

def scanName(isofile):
    # XXX: we don't care to delete these temporary dirs... feel free to fix
    tmpdir1=tempfile.mkdtemp()
    tmpdir2=tempfile.mkdtemp()
    tmpcnf='%s/system.cnf' % tmpdir2
    resname='%s/fname' % tmpdir2

    cmd = """
/bin/mount -o loop -t iso9660 %(iso)s %(tmpdir1)s
/bin/cp %(tmpdir1)s/system.cnf %(tmpcnf)s
/bin/cat %(tmpcnf)s | perl -e 'while(<STDIN>) {if (m/^BOOT2(.*?):\\\\(.*?);(.*?)$/) { print $2 }}' > %(res)s
/bin/umount %(tmpdir1)s
""" % {'iso':isofile, 'tmpdir1':tmpdir1, 'tmpcnf':tmpcnf, 'res':resname}
    os.system(cmd)

    f = open(resname, 'rb')
    r = f.read(1024)  # FIXME
    f.close(); f = None
    return r

def copySplit(isofile, dispname, fname, destdir):
    dummy_id = crc(dispname + '\0')
    destbasename = 'ul.' + dummy_id + '.' + fname

    f = open(isofile, 'rb')
    parts = 0
    (fo, folen) = (None, 0)
    while True:
        d = f.read(64*1024*1024)  # 64M
        if len(d) == 0:
            if fo != None:
                fo.close(); fo = None
                print '  --> %d bytes' % folen
            break
        
        if (fo == None) or (folen == 1024*1024*1024):
            if fo != None:
                fo.close(); fo = None
                print '  --> %d bytes' % folen
            nextpart = destbasename + ('.%d%d' % (parts / 10, parts % 10))
            print 'part %d -> %s' % (parts, nextpart)
            parts = parts + 1
            fo = open(destdir + '/' + nextpart, 'wb')
            folen = 0

        fo.write(d)
        folen = folen + len(d)
    f.close(); f = None
    return ('ul.' + fname, parts)

# XXX: this generates multiple backups but since they're small, we don't care
def backupIndex(destdir):
    r = random.Random()
    idxbackup = '%s/ul.backup.%d' % (destdir, r.randrange(0,stop=1000000))
    os.system('cp %s/ul.cfg %s' % (destdir, idxbackup))
    return idxbackup

def updateIndex(dispname, ulname, parts, destdir):
    newdata = struct.pack('!32s15sB', dispname, ulname, parts) + \
        '\x14\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
    f = open('%s/ul.cfg' % destdir, 'ab')
    f.write(newdata)
    f.close(); f = None
    return None

# --------------------------------------------------------------------------

print 'ulcopyiso.py 0.1 - (c) sami.vaarala (at) iki.fi, 2005'

try:
    dispname = sys.argv[1]
    isofile = sys.argv[2]
    destdir = sys.argv[3]
except:
    print """
Ulcopyiso.py performs tasks similar to ul_install.exe, but works under
Linux.  Uses Linux specific stuff (like loop mounts), so it won't
work on other platforms.

Usage: ulcopyiso.py <dispname> <isoname> <usbextreme dir>

    <dispname>        Display name of backup (shown in usbextreme menu)
    <isoname>         Name of ISO backup file
    <usbextreme dir>  Path to usbextreme hard drive

Example:

    $ ./ulcopyiso.py my-backup backup.iso /mnt/my-usb-disk
"""
    sys.exit(1)

print 'Scanning for ELF name...',
fname = scanName(isofile)
print "'" + fname + "'"

print "Copying and splitting ISO file '%s'..." % isofile
(ulname, parts) = copySplit(isofile, dispname, fname, destdir)

print 'Creating backup of ul.cfg index file...',
backupfile = backupIndex(destdir)
print "'" + backupfile + "'"

print 'Updating ul.cfg index file...',
updateIndex(dispname, ulname, parts, destdir)
print 'ok'

print '*** All done ***'
sys.exit(0)
